Skip to content

feat(decisioning): PostgresTaskRegistry — durable HITL task state (v6.1)#361

Merged
bokelley merged 2 commits intomainfrom
claude/issue-353-postgres-task-registry
May 2, 2026
Merged

feat(decisioning): PostgresTaskRegistry — durable HITL task state (v6.1)#361
bokelley merged 2 commits intomainfrom
claude/issue-353-postgres-task-registry

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 2, 2026

Closes #353

Summary

Production sellers running ADCP_ENV=production were blocked from HITL flows because InMemoryTaskRegistry.is_durable=False trips the production-mode gate in create_adcp_server_from_platform. The gate error message literally says "v6.1 ships PostgresTaskRegistry" — this PR delivers it.

Adds PostgresTaskRegistry: a durable TaskRegistry implementation backed by a decisioning_tasks Postgres table. Wiring is a one-line change for adopters:

from psycopg_pool import AsyncConnectionPool
from adcp.decisioning import PostgresTaskRegistry, serve

async with AsyncConnectionPool("postgresql://...") as pool:
    registry = PostgresTaskRegistry(pool=pool)
    await registry.create_schema()  # idempotent; safe on every boot
    serve(MyPlatform(), registry=registry)

Implementation note — psycopg over SQLAlchemy: The issue referenced "SQLAlchemy ORM model." The TaskRegistry Protocol is fully async; psycopg_pool.AsyncConnectionPool (already in the [pg] extra) is a natural drop-in with no new dependency, while SQLAlchemy async ORM would add a heavyweight dep and an AsyncSession lifecycle that doesn't compose cleanly with the Protocol's one-connection-per-call shape. Both expert reviewers converged on psycopg.

Files changed

File Purpose
src/adcp/decisioning/pg/__init__.py New subpackage
src/adcp/decisioning/pg/task_registry.py PostgresTaskRegistry implementation
src/adcp/decisioning/pg/decisioning_tasks.sql Raw DDL for migration-tool adopters (Alembic, Flyway)
src/adcp/decisioning/__init__.py Conditional import stub (always importable, raises at instantiation if [pg] not installed)
pyproject.toml Add decisioning/pg/*.sql to package-data; update [pg] extra comment
tests/test_decisioning_pg_task_registry.py Unit tests (no real DB: Protocol conformance, is_durable, stub import, account_id validation)
tests/conformance/decisioning/test_pg_task_registry.py Real-DB conformance suite (skips unless ADCP_PG_TEST_URL set)

Key correctness properties

  • Cross-tenant isolation at SQL level: get() uses WHERE task_id = %s AND (%s IS NULL OR account_id = %s) — account scoping is in the WHERE predicate, not Python-level filter after fetch.
  • Atomic terminal transitions: complete() and fail() use UPDATE ... WHERE state NOT IN ('completed', 'failed') RETURNING task_id. If zero rows return, a follow-up SELECT determines unknown vs. idempotent vs. conflict — no TOCTOU across workers.
  • Terminal-state guard on progress: update_progress() uses WHERE state NOT IN ('completed', 'failed') so straggler writes can't resurrect completed tasks.
  • is_durable: ClassVar[bool] = True set at class level (not instance), matching what serve.py:194 checks.
  • account_id validation in issue() copied verbatim from InMemoryTaskRegistry — rejects empty, whitespace, and "<unset>" values.

What's tested

  • pytest tests/test_decisioning_pg_task_registry.py — runs without real DB; covers stub import, is_durable, account_id validation, Protocol structural matching
  • tests/conformance/decisioning/test_pg_task_registry.py — full lifecycle, cross-tenant security, idempotency, concurrent issue — requires ADCP_PG_TEST_URL

Note: The pg-replay-store CI job should be extended to also run tests/conformance/decisioning/test_pg_task_registry.py (cannot edit .github/workflows from triage PRs — flagged as a nit for human reviewer).

Pre-PR review

  • code-reviewer: approved after fixing — _table internal param added for test isolation, pre-formatted SQL, unused logging removed, stub ClassVar annotation corrected, missing complete/fail(unknown) conformance tests added.
  • dx-expert: approved — adopt path clear, create_schema() + raw DDL file covers both boot-and-go and migration-tool adopters, from adcp.decisioning import PostgresTaskRegistry is discoverable. SQL file now in wheel via pyproject.toml fix.

Nits surfaced (not fixed in this PR)

  • CI pg-replay-store job doesn't run decisioning conformance tests (.github/workflows edit needed — out of triage PR scope)
  • No example file showing Postgres wiring end-to-end (follow-up)
  • Cross-terminal behavior (complete() after fail()) undocumented in Protocol — follow-up to align InMemoryTaskRegistry and document in Protocol docstring

Triage-managed PR. This bot does not currently iterate on
review comments or PR conversation threads (only on the source
issue). To unblock:

  • Push fixup commits directly: gh pr checkout <num>
    fix → push.
  • Or re-trigger: comment /triage execute on the source
    issue.

See adcp#3121
for context.

Session: https://claude.ai/code/session_01L8ig1NtL6WZQcYge3RKrb3


Generated by Claude Code

claude added 2 commits May 2, 2026 16:10
Closes #353

Implements the durable `TaskRegistry` promised in the v6.1 plan. Production
adopters running `ADCP_ENV=production` were blocked from HITL flows because
`InMemoryTaskRegistry.is_durable=False` trips the production-mode gate in
`create_adcp_server_from_platform`. This PR delivers the Postgres-backed
alternative.

**Implementation note:** Uses `psycopg_pool.AsyncConnectionPool` (the existing
`[pg]` extra) rather than SQLAlchemy ORM. The `TaskRegistry` Protocol is fully
async; the async psycopg pool is a drop-in with no new dependency. See PR body
for rationale.

https://claude.ai/code/session_01L8ig1NtL6WZQcYge3RKrb3
- Add internal _table parameter + pre-formatted SQL (enables test isolation,
  mirrors PgReplayStore.table_name pattern without public API)
- Add decisioning/pg/*.sql to pyproject.toml package-data so DDL ships in wheel
- Fix conformance test fixture to use _table for per-test table isolation
- Add complete/fail unknown-task raise tests to conformance suite
- Fix stub ClassVar annotation in decisioning __init__.py
- Remove unused logging import from task_registry.py
- Update pg extra comment in pyproject.toml to reference PostgresTaskRegistry

https://claude.ai/code/session_01L8ig1NtL6WZQcYge3RKrb3
@bokelley bokelley force-pushed the claude/issue-353-postgres-task-registry branch from 8d9d05c to ec4a1b1 Compare May 2, 2026 20:13
@bokelley bokelley marked this pull request as ready for review May 2, 2026 20:19
@bokelley bokelley merged commit e01eef0 into main May 2, 2026
20 of 21 checks passed
@bokelley bokelley deleted the claude/issue-353-postgres-task-registry branch May 2, 2026 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(decisioning): Postgres TaskRegistry reference (v6.1)

2 participants